本文會提到
範疇的運作方式有兩種-語彙範疇(lexical scope)和動態範疇(dynamic scope),在這裡先來探討「語彙範疇」。
語彙分析階段會將字串解析成 token,例如:var a = 2;
會解析為 var
、a
、=
、2
、;
。語彙範疇是在語彙分析時期所定義的範疇,而範疇的劃分在程式碼撰寫時就決定好了,之後任何企圖修改的行為都是不恰當的。
參考以下程式碼,試著區分有幾個範疇?誰是誰的巢狀範疇?
function foo(a) {
var b = a * 2;
function bar(c) {
console.log(a, b, c);
}
bar(b * 3);
}
foo(2); // 2 4 12
答案是...
...
...
...
圖片來源:You Don't Know JS: Scope & Closures, Chapter 2: Lexical Scope
這裡有三個範疇...
從上例可知,範疇的劃分說明了 JavaScript 引擎如何尋找識別字的所在之處。
這裡還要談兩個觀念「遮蔽(shadowing)」和「全域變數(global variable)」。
window.a
來避免 a 被巢狀範疇內層的同名變數遮蔽。備註:範疇的查找只適用於一級識別字,例如:a、b 這樣單層的名稱。如果是要找 foo.bar.a 的話,範疇的查找只會找到 foo,之後的 bar 和 a 就會由物件存取規則(object property-access rules)來繼續解析。
有兩個方法會在執行時修改語彙範疇-eval 和 with。
範例如下,在 foo 內執行 eval,導致 console.log(...)
時 JavaScript 引擎尋找 b 時在 foo 這個範疇找到(其值為 3),而遮蔽了全域的 b(其值為 2)。
function foo(str, a) {
eval(str);
console.log(a, b);
}
var b = 2;
foo('var b = 3;', 1); // 1 3
...
...
eval 很邪惡,好孩子不要用!
...
...
with 會在執行時期創建新的語彙範疇,這裡來看一個全域值外漏的例子。
當 with 區塊執行時,with 將物件參考當成範疇來看,這個物件的特性就會成為該範疇內的識別字。因此,a = 2
其實是在做 LHS 的動作,若在 o2 和 foo 的範疇找不到 a,就會往全域範疇來找,由於在此並非嚴格模式,因此在找不到的情況下,就會生出一個全域變數 a 並設定其值為 2。
function foo(obj) {
with (obj) {
a = 2;
}
}
var o1 = {
a: 3
};
var o2 = {
b: 3
};
foo(o1);
console.log(o1.a); // 2
foo(o2);
console.log(o2.a); // undefined
console.log(a); // 2,全域值外漏
...
...
幸好,with 已被禁止使用了。
...
...
JavaScript 引擎會在編譯時期進行最佳化,例如,靜態分析程式碼,確定變數和函式的宣告,這樣在執行時期就能節省解析識別字的成本。
但若在程式碼中有 eval 或 with,剛剛在編譯時期所確認的變數和函式的所在位置的結果都無效了,因為 JavaScript 引擎無法在編譯時期確認到底傳入什麼東西給 eval 或有什麼內容會讓 with 創建新的語彙範疇,所以也就不知道有什麼會改變語彙範疇了,也就是說,剛剛所做的最佳化都沒有意義了,JavaScript 引擎可考慮乾脆不要最佳化,因此程式碼就會跑得比較慢、效能比較差。
看完這篇文章,我們到底有什麼收穫呢?藉由本文可以理解到...
window.x
來避免被內層變數遮蔽;範疇的查找只適用於單層的識別字名稱,若為多層則是由物件存取規則來做解析。同步發表於部落格。